#!/usr/bin/python2.4

# pylint: disable-msg=C6304

# W0212: Access to protected member (sleazy)
# pylint: disable-msg=W0212

"""Manipulates Wind barbs."""

import math
import StringIO
import sys

from matrix import BitMatrix
from matrix import BresenhamLine
from matrix import BresenhamLineFloats
from matrix import Point
from matrix import PointSet
from matrix import Polar
from matrix import TransformStack
import vectorfield
import uwimagereader


class ComparingPointSet(PointSet):
  """A point set, but counts the number of points in common or missing."""

  def __init__(self, base_m):
    """Constructor.

    Args:
      base_m: A PointSet; responds to GetPixel method.
    """
    super(ComparingPointSet, self).__init__()
    self._base_m = base_m
    self._points_in_common = 0
    self._points_missing = 0

  def SetPixel(self, point, value):
    pixel_in_image = self._base_m.GetPixel(point)
    if pixel_in_image is None:
      pixel_in_image = False
    if value == pixel_in_image:
      self._points_in_common += 1
    else:
      self._points_missing += 1


class TidalVectorGenerator(object):
  """Generates Tidal Current Vectors."""

  def __init__(self, handle, s, true_tide, color):
    s.Push()  # {

    sf = 20.0 * true_tide.Magnitude()
    s.Scale(sf, -sf)  # compensate for mirrored X11 Y coordinate system
    # TODO(rrh): fully understand the rotation here.
    # I originally rotated by 0.5 * pi, but 1.5*pi (270 degrees rotated)
    # displayed a 90 degree tidal vector with the arrow pointing to the R.
    # s.Rotate(0.5 * math.pi + math.radians(-true_tide.Angle()))
    s.Rotate(1.5 * math.pi + math.radians(-true_tide.Angle()))

    p0 = s.Transform(Point(0.0, 0.0))
    p1 = s.Transform(Point(1.0, 0.0))
    self.DrawLine(handle, p0, p1, color)

    # Empirically, the tip/head of the arrow wants to be at 0,0
    p0 = s.Transform(Point(0.0, 0.0))
    p1 = s.Transform(Point(0.3, 0.3))
    self.DrawLine(handle, p0, p1, color)

    s.Pop()  # }


class WindBarbGenerator(object):
  """Generates windbarbs by drawing lines, writing to handle."""

  def __init__(self, handle, s, true_wind, color,
               rotation=60, barbette_length=0.5, barbette_spacing=0.20):
    """Draw a windbarb.

    Args:
      handle: A handle passed through the caller to pass on to the line drawer,
        typically a PointSet.
      s: A transformation stack used to extract transformed coordinates.
      true_wind: A Polar with the the true_wind_speed (kts) and angle.
      color: The color to use for the barb
      rotation: The rotation angle used between the shaft and the fletchings.
      barbette_length: The length of the barbette (fletching) relative to
        the shaft length.
      barbette_spacing: The distance between barbettes.
    """
    self._rotation = rotation
    self._barbette_length = barbette_length
    self._barbette_spacing = barbette_spacing

    s.Push()  # {
    tip = s.Transform(Point(0, 0))
    s.Rotate(math.pi/2.0 + math.radians(-true_wind.Angle()))
    s.Translate(Point(1.0, 0.0))
    feather = s.Transform(Point(0.0, 0))
    nfullbarbs = int(math.floor(true_wind.Mag())) / 10
    if true_wind.Mag() >= nfullbarbs * 10.0 + 5.0:
      nhalfbarbs = 1
    else:
      nhalfbarbs = 0
    nbarbs = nfullbarbs + nhalfbarbs

    self.DrawLine(handle, tip, feather, color)
    for b in xrange(nbarbs):
      is_halfbarb = (b == nbarbs - 1) and (nhalfbarbs > 0)
      if is_halfbarb:
        barblg = self._barbette_length / 2.0
      else:
        barblg = self._barbette_length
      s.Push()  # {
      s.Rotate(math.radians(self._rotation))
      p0 = s.Transform(Point(0.0, 0.0))
      p1 = s.Transform(Point(barblg, 0.0))
      self.DrawLine(handle, p0, p1, color)
      s.Pop()  # }
      # walk back from feather to tip
      s.Translate(Point(-self._barbette_spacing, 0.0))
    s.Pop()  # }


class RasterWindBarbGenerator(WindBarbGenerator):
  """Generates windbarbs using a rasterized (pixel) output grid."""

  def DrawLine(self, handle, p0, p1, color):
    """Draw a line from p0 to p1, using the handle.

    Args:
      handle: A handle passed through the caller to pass on to the line drawer,
        typically a PointSet.
      p0: From point.
      p1: To point.
      color: The color to use.
    """
    BresenhamLineFloats(handle, p0, p1, color)


class BestFittingLine(object):
  """A data structure to hold information about a best fitting line."""

  def __init__(self, blob_name, best_p1, p0, best_polar, best_common_len):
    self._blob_name = blob_name
    self._best_p1 = best_p1
    self._p0 = p0
    self._best_polar = best_polar
    self._best_common_len = best_common_len

  def GetBestPolar(self):
    return self._best_polar

  def Print(self, output_fd=sys.stdout):
    print >>output_fd, '%s p1 %s --> p0 %s with %s common %d' % (
        self._blob_name,
        self._best_p1,
        self._p0,
        self._best_polar,
        self._best_common_len)


class TraversalArtifact(object):
  """Keeps track of the points visited when traversing pixels in an arrow."""

  def __init__(self):
    self._excluded = BitMatrix('visited')
    self._allpoints = []
    self._endpoints = []

  def Visit(self, point):
    self._excluded.SetPixel(point)
    self._allpoints.append(point)

  def HaveVisited(self, point):
    return self._excluded.GetPixel(point)

  def MarkAsEndSegment(self, point):
    self._endpoints.append(point)

  def PrintTraversal(self, output_file=sys.stdout):
    print >>output_file, 'endpoints= %d' % (len(self._endpoints),)
    for point in self._endpoints:
      print >>output_file, '  %s' % point

    print >>output_file, 'allpoints= %d' % (len(self._allpoints),)
    for point in self._allpoints:
      print >>output_file, '  %s' % point

  def FindBestFittingLinesForEndPoints(self, blob_name, xscale=1.0, yscale=1.0):
    """Find the best fitting lines for self._allpoints.

    Args:
      blob_name: The blob name; used only for marking up output for tracing.
      xscale: x scale factor to compensate for distortions of the projection.
      yscale: y scale factor to compensate for distortions of the projection.

    Returns:
      A list of BestFittingLine.
    """
    return_list = []
    allpoints_set = set(self._allpoints)
    for p0 in self._endpoints:
      best_common_len = 0
      best_p1 = None
      for p1 in self._allpoints:
        if p0 == p1:
          continue
        result_set = PointSet()
        BresenhamLine(result_set, p0, p1, True)
        shared = allpoints_set.intersection(result_set.GetSet())
        len_shared = len(shared)
        if len_shared > best_common_len:
          best_common_len = len_shared
          best_p1 = p1
      length = best_p1.DistanceFrom(p0)
      angle = best_p1.AngleFrom(p0, xscale, yscale)
      angle = math.fmod(angle, 360)
      best_polar = Polar(int(round(length)), int(round(angle)))

      result_info = BestFittingLine(
          blob_name, best_p1, p0, best_polar, best_common_len)
      return_list.append(result_info)

    return return_list


def UnitAdjacents():
  return [
      (-0, -1),
      (-1, -1),
      (-1, +0),
      (-1, +1),
      (-0, +1),
      (+1, +1),
      (+1, +0),
      (+1, -1),
      ]


def ExploreLine(m, depth, start, traversal):
  """Traverse along a line by exploring all pixels surrounding start.

  This is a recursive procedure.

  Args:
    m: a BitMatrix holding the rasterized image.
    depth: Recursive depth of the exploration.
    start: A starting point.
    traversal: A TraversalArtifact to update with results.
  """
  # The starting point has to be marked.
  if m.GetPixel(start):
    traversal.Visit(start)
  else:
    return

  adjacent_count = 0
  for delta in UnitAdjacents():
    xn = start.x + delta[0]
    yn = start.y + delta[1]
    new_start = Point(xn, yn)
    if m.GetPixel(new_start):
      adjacent_count += 1
      if not traversal.HaveVisited(new_start):
        ExploreLine(m, depth + 1, new_start, traversal)

  # adjacency count of 1 is an edge
  # adjacency count of 3 is an intersection where the barblet comes to the main
  if adjacent_count == 1 or adjacent_count == 3:
    if False:
      print 'depth %2d edge at %s adjacent_count=%d' % (
          depth, start, adjacent_count)
  if adjacent_count == 1:
    traversal.MarkAsEndSegment(start)


COLOR_30KTS = (0, 0, 255)  # saturated pure blue
COLOR_25KTS = (255, 127, 127)  # brick orange
COLOR_20KTS = (255, 255, 127)  # yellow
COLOR_15KTS = (255, 204, 153)  # pale orange
COLOR_10KTS = (191, 255, 191)  # pale green
COLOR_5KTS = (191, 191, 255)  # pale purple
COLOR_0KTS = (255, 255, 255)  # white
BLACK = (0, 0, 0)  # features

COLOR_TO_SPEED = {}
COLOR_TO_SPEED[COLOR_30KTS] = 30
COLOR_TO_SPEED[COLOR_25KTS] = 25
COLOR_TO_SPEED[COLOR_20KTS] = 20
COLOR_TO_SPEED[COLOR_15KTS] = 15
COLOR_TO_SPEED[COLOR_10KTS] = 10
COLOR_TO_SPEED[COLOR_5KTS] = 5
COLOR_TO_SPEED[COLOR_0KTS] = 0


def ExploreBaseColors(m, start):
  """Look for majority colors near the tip of the wind barb.

  Args:
    m: a BitMatrix holding the rasterized image.
    start: A starting Point.

  Returns:
    The wind speed (knots) near the tip of the wind barb, determined
    from the majority of colors nearby.
  """
  counts = {}
  for rscale in [1, 2]:  # widening circle of nearby points to examine
    for dx, dy in UnitAdjacents():
      point = Point(start.x + rscale * dx, start.y + rscale * dy)
      color = m.GetPixelRaw(point)
      if color == BLACK:  # features, such as land boundaries
        continue
      counts.setdefault(color, 0)
      counts[color] += 1
    max_count = 0
    max_color = None
    for color in counts:
      if counts[color] > max_count:
        max_count = counts[color]
        max_color = color
    if max_color:
      break
  wind_speed = COLOR_TO_SPEED.get(max_color, -1)
  if False:
    print '%s ==> %s (%d times) %d kts' % (
        start, max_color, max_count, wind_speed)
  return wind_speed


def ExploreWindBarb(m, start, unix_epoch_time,
                    attribution, output_file=sys.stdout):
  """Look for best fitting wind barb, try them all at 5 degree intervals.

  Get the wind speed from the adjacent color, which proved to be more reliable
  and faster than hypothesizing wind speeds and matching barbettes.

  Args:
    m: a BitMatrix holding the rasterized image.
    start: A starting Point.
    unix_epoch_time: The unix epoch time, seconds, for this wind barb.
    attribution: What to put into the attribution field of the CSV record;
      typically will name a ppm file in the appropriate directory.
    output_file: A file-object of where to write CSV data
        describing this windbarb.
  """
  ll = uwimagereader.ConvertUWPixelsToLatLon(start)

  true_wind_speed_from_color = ExploreBaseColors(m, start)

  s = TransformStack()
  s.Translate(start)

  best_true_wind_angle = 0
  best_true_wind_angle_points = 0

  degrees_per_step = 5
  for true_wind_angle_step in xrange(0, 360/degrees_per_step):
    true_wind_angle = true_wind_angle_step * 1.0 * degrees_per_step
    for true_wind_speed in [true_wind_speed_from_color]:
      for rotation in [-60]:
        for barb_length in [11, 12]:
          # barbette_length of 0.50 works best?
          for barbette_length in [0.50]:
            true_wind = Polar(true_wind_speed, true_wind_angle)

            s.Push()  #{

            s.Scale(1, -1)  # Account for X11/Y axis flipping
            s.Scale(barb_length, barb_length)
            points = ComparingPointSet(m)

            # WindBarbGenerator
            RasterWindBarbGenerator(points, s,
                                    true_wind,
                                    color=True,  # black and white only
                                    rotation=rotation,
                                    barbette_length=barbette_length)

            if False:
              print 'common=%d missing=%d' % (
                  points._points_in_common, points._points_missing)
            alltested = points._points_in_common + points._points_missing
            if alltested:
              ratio = points._points_in_common / (1.0*alltested)
            else:
              ratio = float('inf')
            if False:
              fmt = ('%4.2f %3d %3d ==> '
                     '%s TW=%s lg %2d blg %3.1f rot %2d')
              print fmt % (
                  ratio,
                  points._points_in_common,
                  points._points_missing,
                  start,
                  true_wind,
                  barb_length, barbette_length,
                  rotation)

            if best_true_wind_angle_points < points._points_in_common:
              best_true_wind_angle_points = points._points_in_common
              best_true_wind_angle = true_wind_angle

            s.Pop()  # }

  cv = vectorfield.CoordsToVector(
      unix_epoch_time, ll,
      Polar(true_wind_speed_from_color, best_true_wind_angle),
      attribution=attribution)

  print >>output_file, '%s' % (cv.CSVFormat())
